nginx + lua + webSocketでかんたんpub-subサーバ
概要
実験用に簡単に使えるWebSocketバックエンドが欲しかったので、
ngx + ngx_lua_module からのゴリ押しで作ってみた。
sassembla / nginx-luajit-websocket-pubsuber
https://github.com/sassembla/nginx-luajit-websocket-pubsuber
構成
nginx 1.7.10
ngx_lua_module 0.9.15
redis 2.8.9
luajit 2.1 alpha
websocket protocol + server
redis connector
other
動作内容
nginx-luaでは、リクエストごとに完璧にisolateされたluaスクリプト動作が発生する。
たとえばWebSocket受ける場合、
プロセスをwhileでがっちりブロックしてレスポンス返さずにWebSocket接続という形になった。
https://github.com/sassembla/nginx-luajit/blob/master/bin/lua/client.lua#L69
-- start websocket serving
while true do
local recv_data, typ, err = wb:recv_frame()
if wb.fatal then
local jsonData = json:encode({connectionId = serverId, state = STATE_DISCONNECT_1})
pubRedisCon:publish(IDENTIFIER_CENTRAL, jsonData)
で、luaのコンテキストがリクエストごとにisolateされてるとは言っても、pub-subみたいなことをしたい場合、中継がなくて困るので、
中継にredisのpubsubを使っている。
redidのpubsubにpush性能は無く、こちらも単に、特定のキーのsubscribeをwhileループで行っている。
https://github.com/sassembla/nginx-luajit/blob/master/bin/lua/client.lua#L114
-- subscribe loop
-- waiting data from central.
function subscribe ()
while true do
local res, err = subRedisCon:read_reply()
if not res then
ngx.log(ngx.ERR, "redis subscribe read error:", err)
break
else
-- for i,v in ipairs(res) do
-- ngx.log(ngx.ERR, "client i:", i, " v:", v)
-- end
もちろん一個のリクエストプロセス中で2個while書けるはずなくて、ngx_lua_moduleから使えるthread生成を頼っている。
https://github.com/sassembla/nginx-luajit/blob/master/bin/lua/client.lua#L60
function connectWebSocket()
-- start subscribe
ngx.thread.spawn(subscribe)
-- send connected
local jsonData = json:encode({connectionId = serverId, state = STATE_CONNECT})
pubRedisCon:publish(IDENTIFIER_CENTRAL, jsonData)
-- start websocket serving
while true do
local recv_data, typ, err = wb:recv_frame()
中央部分と残念なとこ
redisでpubsubを使って、client x n -> central, central -> client x n を繋いでいる。
つまりredisのpubsubが2系統ある。
で、clientはWebSocket接続なわけだが、centralは、
httpで通信してきたプロセスをredisのcentral pubsubのためにガッチリ拘束する、っていう残念な方法を取っている。
https://github.com/sassembla/nginx-luajit/blob/master/bin/lua/controlpoint.lua#L99
function main ()
-- start waiting loop
while true do
local res, err = subRedisCon:read_reply()
if not res then
ngx.log(ngx.ERR, "failed to receiving data from clients, err:", err)
ngx.exit(500)
return
else
つまり現在の構造は、以下の順でしか動作できない。
1.httpでcentralのパスにアクセス(この通信は帰ってこない)
2.適当にWebSocket接続
3.中央コンテキストでいろいろできる
httpで通信してきただけのやつを停めるのどうなの? とか、それ起動時に自動的にできないの? とか、そのへんを模索しているところ。
副産物
期せずしてredisのpub subに依存したが、これによってclient側へのpushの責務がclient connection側に一任されていて、
送付確認周りをわりと疎結合かつ非同期に作る事が出来た。
つまり一対多のWebSocketのsendにロックがかかっていない。
使っててちょっと楽だ。実用に耐えるとは思ってないけど。
また、redisでの中継でclients - centralを繋いでいるので、clientsからどのpubsubを使うか指定させる事でルームみたいな概念が実現できた。
なかなかおもしろい。
ロックせず、各自のwhileループで回っている部分自体はまあはいって感じなんだけど、redisのpubsubによってタイムラグは若干出ると思う。
client側の実装として、udp版やmqtt版みたいなのも作ってみたい。
まあキューイング部分が全部redisに寄るので、あっさり破綻すると思うけど。
中間にRabbitMQとかくっつけるのが正しい気がする。
エラー処理について
clientは、エラーが起こった瞬間subscribeをヤメて自沈する。
というかぶっちゃけredisについて詳しく知らないので、何がどうなってるのかコード追ったりしてみようと思う。
良く出来てるソフトウェアが多くて助かる。
とりあえず動かせたので
nginxのモジュール書くか、
コンセプトモデルだけこれでOKということにしてnginxを基礎にした別物をCでガンガン書くか、
何もかもなかったことにしてコンセプトをnginxから学びつつgoで遊ぶかの3択中。
ngx_lua、楽しいです。